Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix curlyline rendering in AtlasEngine and GDIRenderer #16444

Merged
merged 7 commits into from
Dec 14, 2023

Conversation

tusharsnx
Copy link
Contributor

@tusharsnx tusharsnx commented Dec 8, 2023

Fixes Curlyline being drawn as single underline in some cases

Detailed Description

  • Curlyline is drawn at all font sizes.
  • We might render a curlyline that is clipped in cases where we don't have enough space to draw a full curlyline. This is to give users a consistent view of Curlylines. Previously in those cases, it was drawn as a single underline.
  • Removed minimum threshold minCurlyLinePeakHeight for Curlyline drawing.
  • GDIRender changes:
    • Underline offset now points to the (vertical) mid position of the underline. Removes redundant underlineMidY calculation inside the draw call.

Closes #16288

Validation Steps Performed

  • underlines are drawn properly in WT and Conhost

PR Checklist

  • Tests added/passed

@microsoft-github-policy-service microsoft-github-policy-service bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Area-Rendering Text rendering, emoji, complex glyph & font-fallback issues Priority-2 A description (P2) Product-Conhost For issues in the Console codebase Product-Terminal The new Windows Terminal. labels Dec 8, 2023
@tusharsnx
Copy link
Contributor Author

Removed the minimum threshold altogether cause it passes the acceptable look check ✅ 😅

image

Curlyline may still render as a Single underline when we run out of space to render one.

@tusharsnx tusharsnx changed the title Fix curlyline rendering in AtlasEngine and GDIRender Fix curlyline rendering in AtlasEngine and GDIRenderer Dec 8, 2023
@DHowett
Copy link
Member

DHowett commented Dec 8, 2023

Thanks for fixing this!

This is kinda strange, but...

I am on a 150% scale display. In conhost, I am seeing a weird discontinuity in curly underline support!

Consolas size 13

image

No curly underline (as expected)

Consolas size 14

image

curly and double underlines appear

consolas size 15 and 16

image image

Curlies go away again (!)

consolas 17

image

They're baa~ack

@DHowett
Copy link
Member

DHowett commented Dec 8, 2023

(Double seems to disappear in GDI occasionally as well, or I can't get it to activate)

Copy link
Member

@lhecker lhecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I just got a couple questions (not those with BTW or FYI).

src/renderer/gdi/paint.cpp Show resolved Hide resolved
@@ -306,36 +306,30 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
{
const auto& font = *p.s->font;

// The max height of Curly line peak in `em` units.
const auto maxCurlyLinePeakHeightEm = 0.075f;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(FYI: This PR is tiny if you suppress whitespace changes.)

src/renderer/gdi/paint.cpp Outdated Show resolved Hide resolved
src/renderer/gdi/paint.cpp Outdated Show resolved Hide resolved
src/renderer/gdi/paint.cpp Show resolved Hide resolved
src/renderer/gdi/state.cpp Outdated Show resolved Hide resolved
Comment on lines 402 to 410
// Since we use GDI pen for drawing, the underline offset should point to
// the center of the underline.
_lineMetrics.underlineOffset += strokeHalfWidth;
_lineMetrics.underlineOffset2 += strokeHalfWidth;

// We want the underline to always be visible and remain within the cell
// bottom, so we clamp the offset to fit just inside.
_lineMetrics.underlineOffset = std::min(_lineMetrics.underlineOffset, maxUnderlineOffset);
_lineMetrics.underlineOffset2 = std::min(_lineMetrics.underlineOffset2, maxUnderlineOffset);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems wrong to me. The offsets have already been clamped prior to this, so no additional clamping should have been necessary. The strokeHalfWidth adjustment should just be accounting for the difference between a PatBlt rendering and a LineTo rendering. If it was correct, then the LineTo rendering should exactly match where the PatBlt would have rendered without that adjustment. If it ends up out of bounds, then that's clearly not the case, so something must be wrong with the strokeHalfWidth calculation. Maybe a rounding issue? I don't know.

Worst case, if you can't get it to work, at least move the strokeHalfWidth adjustment higher up, prior to where we're doing the first clamping (and get rid of this second set of clamping). Because we've got special case handling for the double underline offset, which is potentially going to be broken if you clamp it a second time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think another potential problem is that the maxUnderlineOffset that we're using for clamping is intended to be used with the original PatBlt offsets. Once you add the strokeHalfWidth adjustments, that range is no longer applicable. The only way I can see this working is if we can get that strokeHalfWidth calculation correct.

I'd suggest writing a little test app that renders a bunch of lines of varying widths, using both LineTo and PatBlt. From that you should be able to figure out exactly what offset needs to be applied at each width to get the two renderings to match.

Copy link
Contributor Author

@tusharsnx tusharsnx Dec 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I'm not well-versed in Windows programming in general, so creating an app from scratch would be a different task for me 😅

However, I did experiment with the idea of comparing LineTo and PatBlt, and it made it much more clear what I needed to do.

With this code:

// a fixed offset
const auto cellMidY = std::lround(fontHeight / 2.0f);

// adjust for drawing with DrawStrokedLine()
const auto cellMidYWithHalfStrokeWidth = cellMidY + gsl::narrow_cast<int>(std::floor(_lineMetrics.underlineWidth / 2));

// Select a brush

// draw a line with PatBlt()
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, ptTarget.y + cellMidY, widthOfAllCells, _lineMetrics.underlineWidth));

// Select the dotted pen

// draw a line with LineTo()
RETURN_HR_IF(E_FAIL, !DrawStrokedLine(ptTarget.x, ptTarget.y + cellMidYWithHalfStrokeWidth, widthOfAllCells));

Result:

"`e[4:4m Dotted `e[m"

Screenshot 2023-12-10 112020 Screenshot 2023-12-10 113355

I tried this with other font sizes, and they all show the same result. So, the DrawLine() and DrawStrokedLine() calls with those args are synonymous to each other.

This still seems wrong to me. The offsets have already been clamped prior to this, so no additional clamping should have been necessary.

Yeah, you're right! It wouldn't have been necessary if I had floored strokeHalfWidth before adding it to the two offsets. (I had lround'd it, so that was the issue.)

@tusharsnx
Copy link
Contributor Author

tusharsnx commented Dec 11, 2023

@DHowett

I am on a 150% scale display. In conhost, I am seeing a weird discontinuity in curly underline support!

That's probably due to how font metrics differ going from a small font size to larger ones.

As an example, this table shows how transitioning from 13 to 15 font size in Conhost results in cellBottomGap going from 1 to 0 and back to 1.

font-size cell-size underline offset underline width cell-bottom-gap curlyline as curlyline
13 23 21 1 1 Yes
14 25 23 2 0 No
15 26 23 2 1 Yes

And for a visual demo:

demo

@tusharsnx
Copy link
Contributor Author

I need to revisit all the calculations I did for the curly line in both renderers. Will push the updated code if they need to be fixed.

@tusharsnx
Copy link
Contributor Author

tusharsnx commented Dec 11, 2023

I need you guys to give your thoughts on forcing curlyline height to be at least 1.

What do you think should we do when there's not enough space to render a good Curlyline:

  1. Draw a straight line?

image
MS Gothic (20pt)

  1. Draw a curly line of 1px peak height?

image
MS Gothic (20pt)


(2) has several benefits. Some fonts that otherwise would never draw a curlyline in case (1) (due to how close they draw underlines to the cell bottom) actually draw a curlyline that seemingly looks like there isn't any clipping involved (just don't look at them with eagle eyes).

image
MS Gothic (20pt)

image

Curlyline is always distinguishable from single underline which is what the issuer requested in the linked issue

Technical limitations are understandable, but, assuming that the key selling point of this feature is spellchecking, any ugly/incorrect/clipping/overlapping rendering would be better than nothing at all.

image
MS Gothic (15pt)

image
Consolas (6pt)

@tusharsnx
Copy link
Contributor Author

tusharsnx commented Dec 11, 2023

Some samples for 2nd case from Conhost:

image
MS Gothic (13px)

image
MS Gothic (17px)

@DHowett
Copy link
Member

DHowett commented Dec 11, 2023

I am a big fan of (2), always making sure that we display a curly line. I know we can't move it upwards because that would mean it is discontinuous with the normal underline styles, so this seems totally sufficient to me!

@tusharsnx
Copy link
Contributor Author

I know we can't move it upwards because that would mean it is discontinuous with the normal underline styles

image

Hmm... that can be weird. So, I'm going ahead with (2) 👍

@tusharsnx
Copy link
Contributor Author

Ready for review (again)!

Comment on lines +421 to 424
if (maxDrawableCurlyLinePeakHeight > 0.0f)
{
curlyLinePeakHeight = 0.0f;
_lineMetrics.curlylinePeakHeight = std::min(_lineMetrics.curlylinePeakHeight, maxDrawableCurlyLinePeakHeight);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that this behavior can look somewhat unpleasant when Ctrl+scrolling up and down when using Consolas, because it'll jump in between very squiggly lines and barely squiggly lines all the time. Additionally, and unrelated to your PR, I'm having trouble ever getting the double-underline to separate into two distinct lines.

So, I'm currently doing a couple modifications to this function to bring it closer to the corresponding AtlasEngine code: Using floats throughout the function, unless rounding is needed (= for instance, right before calculating the thin line width) and I'm porting Word's double underline algorithm over. My hope is that all this will allow me to better understand how your current code works and to come up with an idea for making the transition between curly and less-curly lines "smoother". (I'll submit the above changes in a different PR after this one merges.)

Personally speaking I don't agree with this:

I know we can't move it upwards because that would mean it is discontinuous with the normal underline styles, so this seems totally sufficient to me!

I think discontinuous lines are better, if it avoids clipping. But I also get that discontinuous lines are kind of weird too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I'm currently doing a couple modifications to this function to bring it closer to the corresponding AtlasEngine code.....(I'll submit the above changes in a different PR after this one merges.)

That sounds good to me.

I have a few ideas that I'll just share here:

  1. We could give users an option to let WT/Conhost adjust underline-offfset. We'll make sure curlyline is always drawn perfectly. This is what Wezterm seems to do (can't be turned off).
  2. (Not sure if it works but) Have a separate render pass for drawing underlines after glyphs/background have been drawn. This ensures we can draw underlines at the right offset while avoiding any clipping. (Too much work)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble ever getting the double underline to separate into two distinct lines.

j4james briefly mentioned it in his comment in the original PR that brought doubly underlined rendering support:

Note how some fonts only have enough space for a thicker line, and not two distinct lines, but I think that's OK (XTerm always renders the attribute this way). Also note that this is more common in the GDI renderer because it uses a smaller line height.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just had a chat with Dustin, and we compared different approaches. Word's double-underline algorithm does look fairly well at small and big font sizes and works better with Consolas. Using the thinLineWidth (as in AtlasEngine) for the curly underline also appears to be slightly better visually than the full underlineWidth.

I'll approve this PR because it puts this code in a fantastic state to do further "fine tuning" on it. Thank you so much for working on this! I also didn't want to ask you to make any more changes, since I don't want to put any more unnecessary burden on you. I'll link the follow-up PR here and post screenshots there to compare the changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: #16475

@DHowett
Copy link
Member

DHowett commented Dec 14, 2023

Thanks so much for doing this!

@DHowett DHowett merged commit f5b45c2 into microsoft:main Dec 14, 2023
15 checks passed
DHowett pushed a commit that referenced this pull request Dec 15, 2023
While #16444 left wavy lines in an amazing state already, there were
a few more things that could be done to make GDI look more consistent
with other well known Windows applications.

But before that, a couple unrelated, but helpful changes were made:
* `GdiEngine::UpdateFont` was heavily modified to do all calculations
  in floats. All modern CPUs have fast FPUs and even the fairly slow
  `lroundf` function is so fast (relatively) nowadays that in a cold
  path like this, we can liberally call it to convert back to `int`s.
  This makes intermediate calculation more accurate and consistent.
* `GdiEngine::PaintBufferGridLines` was exception-unsafe due to its
  use of a `std::vector` with catch clause and this PR fixes that.
  Additionally, the vector was swapped out with a `til::small_vector`
  to reduce heap allocations. (Arena allocators!)
* RenderingTests was updated to cover styled underlines

With that in place, these improvements were done:
* Word's double-underline algorithm was ported over from `AtlasEngine`.
  It uses a half underline-width (aka `thinLineWidth`) which will now
  also be used for wavy lines to make them look a bit more filigrane.
* The Bézier curve for wavy/curly underlines was modified to use
  control points at (0.5,0.5) and (0.5,-0.5) respectively. This results
  in a maxima at y=0.1414 which is much closer to a sine curve with a
  maxima at 1/(2pi) = 0.1592. Previously, the maxima was a lot higher
  (roughly 4x) depending on the aspect ratio of the glyphs.
* Wavy underlines don't depend on the aspect ratio of glyphs anymore.
  This previously led to several problems depending on the exact font.
  The old renderer would draw exactly 3 periods of the wave into
  each cell which would also ensure continuity between cells.
  Unfortunately, this meant that waves could look inconsistent.
  The new approach always uses the aforementioned sine-like waves.
* The wavy underline offset was clamped so that it's never outside of
  bounds of a line. This avoids clipping.

## Validation Steps Performed
* Compile RenderingTests and run it
* Using Consolas, MS Gothic and Cascadia Code while Ctrl+Scrolling
  up and down works as expected without clipping ✅
DHowett pushed a commit that referenced this pull request Dec 15, 2023
Fixes Curlyline being drawn as single underline in some cases

**Detailed Description**

- Curlyline is drawn at all font sizes.
- We might render a curlyline that is clipped in cases where we don't
have enough space to draw a full curlyline. This is to give users a
consistent view of Curlylines. Previously in those cases, it was drawn
as a single underline.
- Removed minimum threshold `minCurlyLinePeakHeight` for Curlyline
drawing.
- GDIRender changes:
- Underline offset now points to the (vertical) mid position of the
underline. Removes redundant `underlineMidY` calculation inside the draw
call.

Closes #16288

(cherry picked from commit f5b45c2)
Service-Card-Id: 91349182
Service-Version: 1.19
DHowett pushed a commit that referenced this pull request Dec 15, 2023
While #16444 left wavy lines in an amazing state already, there were
a few more things that could be done to make GDI look more consistent
with other well known Windows applications.

But before that, a couple unrelated, but helpful changes were made:
* `GdiEngine::UpdateFont` was heavily modified to do all calculations
  in floats. All modern CPUs have fast FPUs and even the fairly slow
  `lroundf` function is so fast (relatively) nowadays that in a cold
  path like this, we can liberally call it to convert back to `int`s.
  This makes intermediate calculation more accurate and consistent.
* `GdiEngine::PaintBufferGridLines` was exception-unsafe due to its
  use of a `std::vector` with catch clause and this PR fixes that.
  Additionally, the vector was swapped out with a `til::small_vector`
  to reduce heap allocations. (Arena allocators!)
* RenderingTests was updated to cover styled underlines

With that in place, these improvements were done:
* Word's double-underline algorithm was ported over from `AtlasEngine`.
  It uses a half underline-width (aka `thinLineWidth`) which will now
  also be used for wavy lines to make them look a bit more filigrane.
* The Bézier curve for wavy/curly underlines was modified to use
  control points at (0.5,0.5) and (0.5,-0.5) respectively. This results
  in a maxima at y=0.1414 which is much closer to a sine curve with a
  maxima at 1/(2pi) = 0.1592. Previously, the maxima was a lot higher
  (roughly 4x) depending on the aspect ratio of the glyphs.
* Wavy underlines don't depend on the aspect ratio of glyphs anymore.
  This previously led to several problems depending on the exact font.
  The old renderer would draw exactly 3 periods of the wave into
  each cell which would also ensure continuity between cells.
  Unfortunately, this meant that waves could look inconsistent.
  The new approach always uses the aforementioned sine-like waves.
* The wavy underline offset was clamped so that it's never outside of
  bounds of a line. This avoids clipping.

* Compile RenderingTests and run it
* Using Consolas, MS Gothic and Cascadia Code while Ctrl+Scrolling
  up and down works as expected without clipping ✅

(cherry picked from commit 99193c9)
Service-Card-Id: 91356394
Service-Version: 1.19
@tusharsnx tusharsnx deleted the fix-curlyline-rendering branch March 18, 2024 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Rendering Text rendering, emoji, complex glyph & font-fallback issues Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-2 A description (P2) Product-Conhost For issues in the Console codebase Product-Terminal The new Windows Terminal.
Projects
Development

Successfully merging this pull request may close these issues.

Curly underline is rendered as single
4 participants